package ca.tatham.triplipse.views.tree;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.ui.IViewSite;
import org.openrdf.model.Resource;
import org.openrdf.model.URI;
import org.openrdf.model.Value;

import ca.tatham.triplipse.Activator;
import ca.tatham.triplipse.RdfDocument;
import ca.tatham.triplipse.RdfStatement;
import ca.tatham.triplipse.views.RdfDocumentContentProvider;

class TreeRdfDocumentContentProvider extends RdfDocumentContentProvider implements
		ITreeContentProvider, ISelectionChangedListener {

	private final IViewSite m_viewSite;
	private TreeParent m_invisibleRoot;

	public TreeRdfDocumentContentProvider(StructuredViewer viewer,
			IViewSite aViewSite) {
		m_viewSite = aViewSite;
		viewer.addPostSelectionChangedListener(this);
	}

	private IViewSite getViewSite() {
		return m_viewSite;
	}

	@Override
	protected void filterChanged() {
		m_invisibleRoot = null;
		refresh();
	}

	public Object[] getElements(Object parent) {
		if (parent.equals(getViewSite())) {
			if (m_invisibleRoot == null) {
				populateTree();
			}
			return getChildren(m_invisibleRoot);
		}
		return getChildren(parent);
	}

	public Object getParent(Object child) {
		if (child instanceof TreeObject) {
			return ((TreeObject) child).getParent();
		}
		return null;
	}

	public Object[] getChildren(Object parent) {
		if (parent instanceof TreeParent) {
			return ((TreeParent) parent).getChildren();
		}
		return new Object[0];
	}

	public boolean hasChildren(Object parent) {
		if (parent instanceof TreeParent) {
			return ((TreeParent) parent).hasChildren();
		}
		return false;
	}

	@Override
	public void rdfChanged() {
		populateTree();
		super.rdfChanged();
	}

	protected RdfStatement getTriple(TreeObject node) {
		switch (node.getNodeType()) {
		case SUBJECT:
			if (node instanceof TreeParent) {
				TreeParent subjectNode = (TreeParent) node;
				TreeParent predicateNode = (TreeParent) subjectNode
						.getChildren()[0];
				TreeObject objectNode = predicateNode.getChildren()[0];
				return getTriple(subjectNode, predicateNode, objectNode);
			}
			return null;
		case PREDICATE:
			if (node instanceof TreeParent) {
				TreeParent predicateNode = (TreeParent) node;
				TreeParent subjectNode = predicateNode.getParent();
				TreeObject objectNode = predicateNode.getChildren()[0];
				return getTriple(subjectNode, predicateNode, objectNode);
			}
			return null;
		case INVISIBLE:
			return null;
		case OBJECT_REF_LEAF:
		case OBJECT_REF_PARENT:
		case OBJECT_LITERAL:
			TreeObject objectNode = node;
			TreeObject predicateNode = objectNode.getParent();
			TreeObject subjectNode = predicateNode.getParent();
			return getTriple(subjectNode, predicateNode, objectNode);
		}
		return null;
	}

	private RdfStatement getTriple(TreeObject subjectNode,
			TreeObject predicateNode, TreeObject objectNode) {
		Resource subject = (Resource) subjectNode.getName();
		URI predicate = (URI) predicateNode.getName();
		Value object = objectNode.getName();
		RdfStatement rdfStatement = getRdfDocument().getTriple(subject, predicate, object);
		return rdfStatement;
	}

	/**
	 * Populate the tree with data from the RdfDocument. Each node in the tree
	 * is either a Subject, Predicate, or Object.
	 * 
	 */
	private TreeParent populateTree() {
		TreeParent root = new TreeParent(NodeType.INVISIBLE, null);

		Set<Resource> rootSubjects = getRdfDocument().getUnreferencedSubjects(
				getFilter());

		addTriplesToParent(root, rootSubjects);

		m_invisibleRoot = root;
		return root;
	}

	private RdfDocument getRdfDocument() {
		return Activator.getDefault().getRdfDocument();
	}

	private void addTriplesToParent(TreeParent parent, Set<Resource> subjects) {

		for (Resource subject : subjects) {
			TreeParent subjectNode = new TreeParent(NodeType.SUBJECT, subject);
			parent.addChild(subjectNode);

			Set<Resource> resourcesInBranch = new HashSet<Resource>();

			addTriplesFromSubject(subject, subjectNode, resourcesInBranch);
		}
	}

	private void addTriplesFromSubject(Resource subject,
			TreeParent subjectNode, Set<Resource> resourcesInBranch) {

		boolean alreadyContains = !resourcesInBranch.add(subject);

		if (alreadyContains) {
			subjectNode.setCycle(true);
			return;
		}
		RdfDocument doc = getRdfDocument();
		Collection<RdfStatement> rdfStatements = doc.getTriplesBySubject(subject);
		Map<Resource, Collection<RdfStatement>> byPredicate = getPredicateMap(rdfStatements);
		for (Resource predicate : byPredicate.keySet()) {
			TreeParent predicateNode = new TreeParent(NodeType.PREDICATE,
					predicate);
			subjectNode.addChild(predicateNode);
			for (RdfStatement rdfStatement : byPredicate.get(predicate)) {
				Value object = rdfStatement.getObject();
				if (object instanceof Resource) {
					addObjectNode(predicateNode, (Resource) object,
							resourcesInBranch);
				} else {
					addObjectLeaf(NodeType.OBJECT_LITERAL, predicateNode,
							object);
				}
			}
		}
	}

	/**
	 * Add the given object to the predicateNode, either as a leaf or as a
	 * parent with all of its subsequent triples in the RDF graph
	 * 
	 * @param doc
	 * @param predicateNode
	 * @param object
	 */
	private void addObjectNode(TreeParent predicateNode, Resource object,
			Set<Resource> resourcesInBranch) {
		Collection<RdfStatement> subTriples = getRdfDocument().getTriplesBySubject(
				object);
		if (subTriples != null && subTriples.size() > 0) {
			TreeParent objectNode = new TreeParent(NodeType.OBJECT_REF_PARENT,
					object);
			addTriplesFromSubject(object, objectNode, resourcesInBranch);
			predicateNode.addChild(objectNode);
		} else {
			addObjectLeaf(NodeType.OBJECT_REF_LEAF, predicateNode, object);
		}
	}

	private void addObjectLeaf(NodeType nodeType, TreeParent predicateNode,
			Value object) {
		TreeObject objectNode = new TreeObject(nodeType, object);
		predicateNode.addChild(objectNode);
	}

	private Map<Resource, Collection<RdfStatement>> getPredicateMap(
			Collection<RdfStatement> rdfStatements) {
		Map<Resource, Collection<RdfStatement>> byPredicate = new HashMap<Resource, Collection<RdfStatement>>();
		for (RdfStatement rdfStatement : rdfStatements) {
			if (!includeTriple(rdfStatement)) {
				continue;
			}

			Collection<RdfStatement> list = byPredicate
					.get(rdfStatement.getPredicate());
			if (list == null) {
				list = new ArrayList<RdfStatement>();
				byPredicate.put(rdfStatement.getPredicate(), list);
			}
			list.add(rdfStatement);
		}
		return byPredicate;
	}

	private boolean includeTriple(RdfStatement rdfStatement) {
		if (isFiltered()) {
			return getFilter().includeStatement(rdfStatement);
		}
		return true;
	}

	public void selectionChanged(@SuppressWarnings("unused")
	SelectionChangedEvent event) {
		refresh();
	}

}

/*
 * The content provider class is responsible for providing objects to the view.
 * It can wrap existing objects in adapters or simply return objects as-is.
 * These objects may be sensitive to the current input of the view, or ignore it
 * and always show the same content .
 */
class TreeObject implements IAdaptable {
	private final NodeType m_nodeType;

	private final Value m_name;

	private TreeParent m_parent;

	public TreeObject(NodeType nodeType, Value name) {
		m_nodeType = nodeType;
		m_name = name;
	}

	public NodeType getNodeType() {
		return m_nodeType;
	}

	public Value getName() {
		return m_name;
	}

	public void setParent(TreeParent parent) {
		m_parent = parent;
	}

	public TreeParent getParent() {
		return m_parent;
	}

	public String toString() {
		if (getName() == null) {
			return "";
		}
		return getName().toString();
	}

	public Object getAdapter(@SuppressWarnings("unused")
	Class key) {
		return null;
	}
}

class TreeParent extends TreeObject {

	private ArrayList<TreeObject> m_children;
	private boolean m_cycle = false;

	public TreeParent(NodeType nodeType, Value name) {
		super(nodeType, name);
		m_children = new ArrayList<TreeObject>();
	}

	public void addChild(TreeObject child) {
		m_children.add(child);
		child.setParent(this);
	}

	public void removeChild(TreeObject child) {
		m_children.remove(child);
		child.setParent(null);
	}

	public TreeObject[] getChildren() {
		return m_children.toArray(new TreeObject[m_children.size()]);
	}

	public boolean hasChildren() {
		return m_children.size() > 0;
	}

	public boolean isCycle() {
		return m_cycle;
	}

	public void setCycle(boolean cycle) {
		m_cycle = cycle;
	}
}

enum NodeType {
	INVISIBLE, SUBJECT, PREDICATE, OBJECT_LITERAL, OBJECT_REF_LEAF, OBJECT_REF_PARENT
}
